import argparse
import hashlib
import logging.handlers
import os
import shutil
import smtplib
import sys
import time
from email.header import Header
from email.mime.text import MIMEText
from inspect import isfunction
from shutil import copyfile
from string import Template

logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
fh = logging.handlers.RotatingFileHandler("info-installation.log", mode='a', maxBytes=20 * 1024 * 1024, backupCount=10)
LOG_FORMAT = "[%(asctime)s]:%(levelname)s-[%(funcName)s]:%(message)s"
fh.setFormatter(logging.Formatter(LOG_FORMAT))
logger.addHandler(fh)
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter(LOG_FORMAT))
logger.addHandler(ch)


def url_retrieve(url, dst_path):
    logger.info("download url %s to %s ", url, dst_path)
    if sys.version_info[0] <= 2:
        import urllib
        return urllib.urlretrieve(url, dst_path)
    elif sys.version_info[0] <= 3:
        import urllib.request
        return urllib.request.urlretrieve(url, dst_path)


def tar_compress_file(src_path, dst_path):
    if os.path.isfile(src_path):
        check_file_exists(src_path)
        cmd_with_result('tar -cvzPf {}  {}'.format(dst_path, src_path))
    else:
        check_dir_exists(src_path)
        cmd_with_result('tar -cvzf {} -C {} .'.format(dst_path, src_path))
    check_file_exists(dst_path)


def cmd_remote_trust(dst_username, dst_ip, command):
    shell = 'ssh {}@{} "{}"'.format(dst_username, dst_ip, command)
    logger.debug(">>>>>>>>>>>>>>> exec start: %s", shell)
    result = os.popen(shell).readlines()
    msg = ''
    for line in result:
        line = line.strip('\r\n')
        msg += line
        logger.info(line)
    logger.debug("<<<<<<<<<<<<<<< exec end: %s", shell)
    return msg


def cmd_with_result(shell, is_log=True):
    if is_log:
        logger.debug(">>>>>>>>>>>>>>> exec start: %s", shell)
    proc = os.popen(shell)
    result = proc.readlines()
    output = ''
    for line in result:
        output += line
        line = line.strip('\r\n')
        if is_log:
            logger.info(line)
    ret = proc.close()
    if is_log:
        logger.debug("<<<<<<<<<<<<<<< exec end: %s ret %s", shell, ret)
    return output, ret


def check_str_empty(s, err_msg):
    if s and s.strip() == '':
        msg = "string must not be empty: %s" % err_msg
        logger.critical(msg)
        raise Exception(msg)


def replace_in_file(file_path, src_str, dst_str):
    with open(file_path, 'r+') as f:
        t = f.read()
        t = t.replace(src_str, dst_str)
        f.seek(0, 0)
        f.write(t)
        f.truncate()


# will backup config file before update
# replacement [(src,dst),(src,dst),(src,dst)]
def update_config_files(conf_path, replacements):
    logger.debug("update_config_files (%s) path, replacements (%s)" % (conf_path, str(replacements)))
    bak_path = conf_path + '.bak'
    check_file_exists(conf_path)
    shutil.copy(conf_path, bak_path)
    check_file_exists(bak_path)
    for replace in replacements:
        replace_in_file(conf_path, replace[0], replace[1])
    cmd_with_result("diff %s %s" % (conf_path, bak_path))


def write_file(full_path, content):
    with open(full_path, 'w') as f:
        f.write(content)
    cmd_with_result("ls -la %s" % full_path)


def check_path_end_with(path, ext):
    if not path.endswith(ext):
        msg = "path %s is not a %s file" % (path, ext)
        logger.critical(msg)
        raise Exception(msg)


def make_dir_p(dir_path):
    if not os.path.exists(dir_path):
        os.makedirs(dir_path)
        logger.debug("make dir (%s) path, dir not exists." % dir_path)
    else:
        logger.debug("make dir (%s) path, dir exists.", dir_path)


def check_dir_exists(dir_path, error_msg=""):
    if not os.path.exists(dir_path) or not os.path.isdir(dir_path):
        msg = "%s! dir (%s) not exists, can't be continue" % (error_msg, dir_path)
        logger.critical(msg)
        raise Exception(msg)
    logger.debug("check dir (%s) path, dir exists.", dir_path)


def check_file_exists(file_path, error_msg=""):
    if not os.path.exists(file_path) or not os.path.isfile(file_path):
        msg = "%s! file (%s) not exists, can't be continue" % (error_msg, file_path)
        logger.critical(msg)
        raise Exception(msg)
    logger.debug("check file (%s) path, file exists.", file_path)


# copy files under src folder to dst folder
def copy_folder_files(src_folder, dst_folder):
    if len(src_folder) == 0 or len(dst_folder) == 0:
        logger.debug("folder name is empty,copy src %s to dst %s", src_folder, dst_folder)
        return
    if not os.path.exists(src_folder) or not os.path.exists(src_folder):
        logger.debug("folder is not exist,copy src %s to dst %s", src_folder, dst_folder)
        return
    if os.path.isfile(src_folder) or os.path.isfile(dst_folder):
        logger.debug("src or dst is not folder,copy src %s to dst %s", src_folder, dst_folder)
        return
    cmd_with_result("cp -rf %s/*   %s" % (src_folder, dst_folder))
    cmd_with_result("ls -la  %s" % dst_folder)


def cmd(shell):
    logger.debug(">>>>>>>>>>>>>>> exec start: %s", shell)
    ret = os.system(shell)
    logger.debug("<<<<<<<<<<<<<<< exec end: %s  %s", shell, ret)
    if ret != 0:
        msg = "command run error: %s" % shell
        logger.critical(msg)
        raise Exception(msg)


def remove_file(path):
    if os.path.exists(path):
        logger.debug("file is exist %s", path)
        os.remove(path)
        logger.debug("remove file %s", path)
    else:
        logger.debug("file %s is not exists,need not to remove", path)


def remove_dir(path):
    if os.path.exists(path):
        logger.debug("path is exist %s", path)
        shutil.rmtree(path)
        logger.debug("remove path file %s", path)
    else:
        logger.debug("path %s is not exists,need not to remove", path)


def remove_file_or_dir(path):
    if not os.path.exists(path):
        logger.debug("file or path %s is not exists,need not to remove", path)
        return
    logger.debug("file or path %s is exists", path)
    if os.path.isfile(path):
        os.remove(path)
        logger.debug("remove file %s", path)
    else:
        shutil.rmtree(path)
        logger.debug("remove path file %s", path)


def md5_sum(file_path):
    with open(file_path, 'rb') as f:
        data = f.read()
    md5 = hashlib.md5(data).hexdigest()
    logger.info("file %s md5 %s", file_path, md5)
    return md5


# before copy check src file exist, check dst folder exist, check dst file exists
# check src and dst file md5


def copy_file_to_dir(src_full_path, dst_dir_path):
    check_file_exists(src_full_path)
    check_dir_exists(dst_dir_path)
    dst_full_path = dst_dir_path + os.path.basename(src_full_path)
    logger.info("copy file from %s to %s ", src_full_path, dst_full_path)
    if os.path.exists(dst_full_path):
        logger.info("file %s exist", dst_full_path)
    copyfile(src_full_path, dst_full_path)
    check_file_exists(dst_full_path)
    if md5_sum(src_full_path) != md5_sum(dst_full_path):
        err_msg = "md5 check failed !"
        logger.critical(err_msg)
        raise Exception(err_msg)


def get_val_from_env(env_key, is_check=True):
    value = os.getenv(env_key)
    if value is None or value == "":
        err_msg = "required env is not provided! key: %s  value: %s" % (env_key, value)
        if is_check is False:
            logger.info(err_msg)
            return ""
        else:
            logger.critical(err_msg)
            raise Exception(err_msg)
    return value


def build_args_from_env(env_keys, is_check=True):
    ret = {}
    for key in env_keys:
        value = os.getenv(key)
        if value is None or value == "":
            err_msg = "required env is not provided! key: %s  value: %s" % (key, value)
            if is_check is False:
                logger.info(err_msg)
                continue
            else:
                logger.critical(err_msg)
                raise Exception(err_msg)
        else:
            ret[key] = value
    logger.info("env keys %s", env_keys)
    logger.info("env result %s", ret)
    return ret


def clean_py_script(paths):
    for file in paths:
        remove_file("./%s" % file)
        remove_file("./%sc" % file)


def wget_files(baseurl, files):
    for file in files:
        url_retrieve(baseurl + file, "./" + file)


def build_script_args(dic_args):
    args_list = list(map(lambda item: '--%s="%s"' % (item[0], item[1]), dic_args.items()))
    str_par = " ".join(args_list)
    logger.info(str_par)
    return str_par


def run_py_script(py_file_name, str_args):
    cmd_with_result("python ./%s %s" % (py_file_name, str_args))


def get_args_by_parser(required_args_init, optional_args_init, help_str):
    parser = argparse.ArgumentParser()
    parser.description = help_str
    logger.info("example: %s", help_str)
    for param in required_args_init:
        parser.add_argument(param.pop("names"), required=True, **param)
    for param in optional_args_init:
        parser.add_argument(param.pop("names"), required=False, **param)
    args = parser.parse_args()
    logger.info("args: %s", args)
    return args


def get_str_from_tpl(tpl_str, args_dict):
    ret = Template(tpl_str).substitute(args_dict)
    logger.info(ret)
    return ret


def get_build_number_str(bd_env, str_tpl="%Y%m%d.%H%M%S"):
    return "%s@%s" % (bd_env, time.strftime(str_tpl, time.localtime()))


# make spring boot jar start command
def make_sb_jar_sh_cmd(java_bin_file_path, jvm_args, jar_dir_path, jar_file_name, boot_args, backend_port):
    check_path_end_with(jar_dir_path, os.path.sep)
    check_path_end_with(java_bin_file_path, 'java')
    check_path_end_with(jar_file_name, 'jar')
    sh_tpl = Template(
        '''${java_bin_file_path} ${jvm_args} -jar ${jar_dir_path}${jar_file_name} ${boot_args} --server.port=${backend_port} --spring.config.additional-location=${jar_dir_path}''')
    dst_cmd = sh_tpl.substitute(
        {
            'java_bin_file_path': java_bin_file_path,
            'jar_dir_path': jar_dir_path,
            'jar_file_name': jar_file_name,
            'jvm_args': jvm_args,
            'boot_args': boot_args,
            'backend_port': backend_port
        }
    )
    logger.info(dst_cmd)
    return dst_cmd


def make_py_sh_cmd(python3_bin_path, python_file_path, boot_args=''):
    check_path_end_with(python3_bin_path, 'python3')
    check_path_end_with(python_file_path, '.py')
    sh_tpl = Template('''${python3_bin_path} ${python_file_path} ${boot_args}''')
    dst_cmd = sh_tpl.substitute(
        {
            'python3_bin_path': python3_bin_path,
            'python_file_path': python_file_path,
            'boot_args': boot_args
        }
    )
    logger.info(dst_cmd)
    return dst_cmd


def make_systemctl_timer(time_expression, service_name):
    timer_tpl = Template("""
    # generated automatic at ${current_time}
    [Unit]
    Description=remove file every night

    [Timer]
    OnCalendar=${time_expression}
    Persistent=true
    Unit=${service_name}

    [Install]
    WantedBy=multi-user.target
    """)
    systemd_timer_str = timer_tpl.substitute(
        {'current_time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
         'time_expression': time_expression,
         'service_name': service_name
         }
    )
    logger.info(systemd_timer_str)
    return systemd_timer_str


def make_systemctl_service(description, linux_user, start_path, exec_start_cmd):
    conf_template_systemd = Template('''
    # generated automatic at ${current_time}
    [Unit]
    Description=${description}
    After=syslog.target network.target

    [Service]
    User=${linux_user}
    WorkingDirectory=${start_path}
    ExecStart=${exec_start_cmd}

    [Install]
    WantedBy=multi-user.target
    ''')
    systemd_service_str = conf_template_systemd.substitute(
        {'current_time': time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()),
         'linux_user': linux_user,
         'start_path': start_path,
         'exec_start_cmd': exec_start_cmd,
         'description': description
         }
    )
    logger.info(systemd_service_str)
    return systemd_service_str


def deploy_systemctl_service_or_timer(systemctl_job_name, systemctl_conf_str, start_when_deploy_end=True,
                                      start_when_boot=True):
    if not systemctl_job_name.endswith('timer') and not systemctl_job_name.endswith('service'):
        raise Exception('%s not end with timer or service' % systemctl_job_name)

    dst_path = os.path.join('/etc/systemd/system', systemctl_job_name)
    write_file(dst_path, systemctl_conf_str)
    cmd_with_result("systemctl daemon-reload")
    if start_when_boot:
        cmd_with_result('systemctl enable %s' % systemctl_job_name)
    if start_when_deploy_end:
        cmd_with_result('systemctl start %s' % systemctl_job_name)

    # print next run real time
    if systemctl_job_name.endswith('timer'):
        next_exec, _ = cmd_with_result('systemctl show %s|grep NextElapseUSecRealtime' % systemctl_job_name)
        logger.info('next exec time:[%s]', next_exec)


def begin_with_list_item(path, base_list):
    for base in base_list:
        if path.startswith(base):
            return True
    return False


def copy_to_remote_host(src_file_path, dst_user_name, dst_ip, dst_file_path, cal_md5=True, dst_port=22):
    logger.info("copy_to_remote_host src: {} ".format(src_file_path))
    logger.info("copy_to_remote_host dst:{} {} {} {}".format(dst_user_name, dst_ip, dst_port,
                                                             dst_file_path))
    msg = "\r\n" + src_file_path
    check_file_exists(src_file_path)
    if cal_md5:
        msg = msg + "\r\n" + md5_sum(src_file_path)
    scp_cmd = 'scp -P {} {} {}@{}:{}'.format(dst_port, src_file_path, dst_user_name, dst_ip,
                                             dst_file_path)
    cmd_with_result(scp_cmd)
    msg = msg + "\r\n" + scp_cmd
    if cal_md5:
        msg = msg + "\r\n" + cmd_remote_trust(dst_user_name, dst_ip, 'md5sum {}'.format(dst_file_path))
    return msg


# mysqldump -h 127.0.0.1 -P 3306 -u root -pxxxx  --single-transaction dbname >mysql.db
def mysql_dump_to_file(ip, port, user, pg_psw, dbname, dst_file):
    logger.info(
        "mysql_dump_to_file: {} {} {} {} {}".format(ip, port, user, dbname, dst_file))
    cmd_with_result(
        'mysqldump -h {} -P {} -u {} -p{} --single-transaction {} > {}'.format(ip, port, user, pg_psw, dbname,
                                                                               dst_file))
    check_file_exists(dst_file)


def check_path_exists(path, error_msg=""):
    if not os.path.exists(path):
        msg = "%s! path (%s) not exists, can't be continue" % (error_msg, path)
        logger.critical(msg)
        raise Exception(msg)
    logger.debug("check path (%s) , path exists.", path)


# mongodump --host 127.0.0.1 --port 7017 -u admin -p xxx -d dbname --out /opt/dbname
def mongo_dump_to_file(ip, port, user, dbname, pg_psw, dst_folder):
    logger.info(
        "mongo_dump_to_file: {} {} {} {} {}".format(ip, port, user, dbname, dst_folder))
    cmd_with_result(
        'mongodump --host {} --port {} -u {} -d {} -p {} --out {}'.format(ip, port, user, dbname, pg_psw,
                                                                          dst_folder))
    check_path_exists(dst_folder)


def pg_dump_to_file(ip, port, user, pg_psw, dbname, dst_file):
    logger.info(
        "pg_dump_to_file: {} {} {} {} {}".format(ip, port, user, dbname, dst_file))
    os.environ["PGPASSWORD"] = pg_psw
    cmd_with_result(
        'pg_dump -v -h {} -p {} -U {} -d {} -f "{}"'.format(ip, port, user, dbname,
                                                            dst_file))
    check_file_exists(dst_file)


def init_py_service_and_timer(sys_argv, python3_bin_path,
                              script_full_path,
                              systemctl_service_name,
                              systemctl_service_description,
                              systemctl_service_linux_user,
                              systemctl_timer_name,
                              systemctl_timer_expression):
    if len(sys_argv) > 1 and sys_argv[1] == 'init':
        py_start = make_py_sh_cmd(python3_bin_path, script_full_path)
        service_conf = make_systemctl_service(systemctl_service_description, systemctl_service_linux_user,
                                              os.path.dirname(script_full_path), py_start)
        deploy_systemctl_service_or_timer(systemctl_service_name, service_conf)
        timer_conf = make_systemctl_timer(systemctl_timer_expression, systemctl_service_name)
        deploy_systemctl_service_or_timer(systemctl_timer_name, timer_conf)
        return True
    return False


def print_custom_vars(args_dict):
    """
    print all vals except inner
    """
    logger.info(">>>>>>>>>>>>>>> print_custom_vars")
    for key in args_dict.keys():
        if key.startswith('__') and key.endswith('__'):
            continue
        if isfunction(args_dict[key]):
            continue
        logger.info("%s : %s" % (key, args_dict[key]))
    logger.info("<<<<<<<<<<<<<<< print_custom_vars")


def send_mail(
        mail_host, mail_user, mail_user_psw, receivers, title, content, is_ssl=True):
    message = MIMEText(content)
    message['From'] = Header(mail_user)
    message['To'] = Header(str(receivers))
    message['Subject'] = Header(title, 'utf-8')
    logger.info('mail info: {} {} {} {}'.format(mail_host, mail_user, receivers, title))
    try:
        smtp = smtplib.SMTP()
        smtp.connect(mail_host, 25)
        if is_ssl:
            smtp = smtplib.SMTP_SSL(mail_host, 465)
        smtp.login(mail_user, mail_user_psw)
        smtp.sendmail(mail_user, receivers, message.as_string())
        smtp.quit()
    except smtplib.SMTPException as e:
        logger.exception("Error: send mail exception")


def get_sys_info():
    return "\r\nifconfig:\r\n{}\r\n systemd timer:\r\n{}\r\n disk info:\r\n{}\r\n\r\n".format(
        cmd_with_result('ifconfig', False)[0],
        cmd_with_result('systemctl list-timers', False)[0],
        cmd_with_result('df -hl', False)[0])


def get_current_script_folder():
    return os.path.dirname(os.path.realpath(__file__))
